#region Copyright & License
/* Tyng.Data
 * Copyright (c) 2007, Paul Tyng, All rights reserved.
 * Code licensed under the BSD License:
 * http://www.opensource.org/licenses/bsd-license.php
 * Downloaded from: http://code.google.com/p/tyngdata/
*/
#endregion

using System;
using System.Collections;
using System.Data;
using System.Data.Common;
using System.Diagnostics;
using System.IO;
using System.Text;

namespace Tyng.Data
{
	/// <summary>
	/// A datareader for character delimited files (eg. CSV) with line breaks separating records.
	/// Caters for 1st row having column headings or column headings array supplied.
	/// Can take an array of System.Type objects to specify column types.
	/// </summary>
	public sealed class CharacterDelimitedDataReader : TextReaderDataReader
	{
        /// <summary>
        /// Delimiter defaults to "," for CSV
        /// </summary>
        public const string DefaultDelimiter = ","; //Defaults to CSV

		private string _delimiter = DefaultDelimiter;
		private Field[] _fields;
		private bool _quotedStrings = true;

        private string[] ReadHeadings(int rowsToSkip)
        {
            for (int i = 0; i < rowsToSkip; i++)
                TextReader.ReadLine();

            string headLine = TextReader.ReadLine();

            if(headLine == null) throw new EndOfStreamException();

            int headingCount = int.MaxValue;
            if (_fields != null) headingCount = _fields.Length;

            return SplitRecord(headLine, _quotedStrings, _delimiter, headingCount);
        }

		#region Constructors

        /// <summary>
        /// Constructor
        /// </summary>
        /// <remarks>
        /// Creates a reader for CSV data with no headers and all string type columns
        /// </remarks>
        /// <param name="reader">TextReader for stream of data</param>
        public CharacterDelimitedDataReader(TextReader reader)
            : base(reader)
        {
        }

		/// <summary>
		/// Constructor for CharacterDelimitedDataReader
		/// </summary>
		/// <param name="reader">TextReader for stream of data</param>
		/// <param name="hasColumnHeadings">Flag indicating whether or not the first row of data is column headings</param>
		/// <param name="delimiter">Column delimiter characters</param>
        public CharacterDelimitedDataReader(TextReader reader, bool hasColumnHeadings, string delimiter)
            : this(reader, int.MaxValue, hasColumnHeadings, delimiter)
        {
        }

		/// <summary>
		/// Constructor for CharacterDelimitedDataReader
		/// </summary>
		/// <param name="reader">TextReader for stream of data</param>
        /// <param name="maxRowsToRead">Maximum number of rows to read from the source</param>
		/// <param name="hasColumnHeadings">Flag indicating whether or not the first row of data is column headings</param>
		/// <param name="delimiter">Column delimiter characters</param>
        public CharacterDelimitedDataReader(TextReader reader, int maxRowsToRead, bool hasColumnHeadings, string delimiter)
            : base(reader, maxRowsToRead)
		{
            if(delimiter != null)
			    _delimiter = delimiter;

			if(hasColumnHeadings)
			{
                string[] columnHeadings = ReadHeadings(0);                

				_fields = new Field[columnHeadings.Length];

                for (int i = 0; i < columnHeadings.Length; i++)
                {
                    string header = columnHeadings[i];

                    if (string.IsNullOrEmpty(header)) header = string.Format("Column{0}", i);

                    _fields[i] = new Field(header);
                }
			}
		}

		/// <summary>
		/// Constructor for CharacterDelimitedDataReader
		/// </summary>
		/// <param name="reader">TextReader for stream of data</param>
		/// <param name="delimiter">Column delimiter characters</param>
		/// <param name="fields">Array of fields in the data</param>
        public CharacterDelimitedDataReader(TextReader reader, string delimiter, params Field[] fields)
            : base(reader)
		{
            if (delimiter != null)
			    _delimiter = delimiter;

			_fields = fields;
		}

		/// <summary>
		/// Constructor for CharacterDelimitedDataReader
		/// </summary>
		/// <param name="reader">TextReader for stream of data</param>
		/// <param name="delimiter">Column delimiter characters</param>
		/// <param name="hasColumnHeadings">Flag indicating whether or not the first row of data is column headings</param>
		/// <param name="columnTypes">Array of types of the columns in the data</param>
        public CharacterDelimitedDataReader(TextReader reader, string delimiter, bool hasColumnHeadings, params Type[] columnTypes)
            : base(reader)
		{
            if (delimiter != null)
			    _delimiter = delimiter;

			_fields = new Field[columnTypes.Length];

			string[] headings = new string[0];

			if(hasColumnHeadings) headings = ReadHeadings(0);

			for(int i = 0; i < columnTypes.Length; i++)
			{
                string header = string.Format("Column{0}", i);

                if (hasColumnHeadings && !string.IsNullOrEmpty(headings[i])) header = headings[i];

				_fields[i] = new Field(header, columnTypes[i]);
			}
		}

		/// <summary>
		/// Constructor for CharacterDelimitedDataReader
		/// </summary>
		/// <param name="reader">TextReader for stream of data</param>
		/// <param name="quotedStrings">Flag indicating whether or not strings in the data are quoted</param>
		/// <param name="hasColumnHeadings">Flag indicating whether or not the first row of data is column headings</param>
		/// <param name="delimiter">Column delimiter characters</param>
        public CharacterDelimitedDataReader(TextReader reader, bool quotedStrings, bool hasColumnHeadings, string delimiter)
            : this(reader, hasColumnHeadings, delimiter)
		{
			_quotedStrings = quotedStrings;
		}

        /// <summary>
        /// Constructor for CharacterDelimitedDataReader
        /// </summary>
        /// <param name="reader">TextReader for stream of data</param>
        /// <param name="maxRowsToRead">The maximum number of rows to read from the reader</param>
        /// <param name="quotedStrings">Flag indicating whether or not strings in the data are quoted</param>
        /// <param name="hasColumnHeadings">Flag indicating whether or not the first row of data is column headings</param>
        /// <param name="delimiter">Column delimiter characters</param>
        public CharacterDelimitedDataReader(TextReader reader, int maxRowsToRead, bool quotedStrings, bool hasColumnHeadings, string delimiter)
            : this(reader, maxRowsToRead, hasColumnHeadings, delimiter)
        {
            _quotedStrings = quotedStrings;
        }

		/// <summary>
		/// Constructor for CharacterDelimitedDataReader
		/// </summary>
		/// <param name="reader">TextReader for stream of data</param>
		/// <param name="quotedStrings">Flag indicating whether or not strings in the data are quoted</param>
		/// <param name="delimiter">Column delimiter characters</param>
		/// <param name="fields">Array of fields in the data</param>
        public CharacterDelimitedDataReader(TextReader reader, bool quotedStrings, string delimiter, params Field[] fields)
            : this(reader, delimiter, fields)
		{
			_quotedStrings = quotedStrings;
		}

		/// <summary>
		/// Constructor for CharacterDelimitedDataReader
		/// </summary>
		/// <param name="reader">TextReader for stream of data</param>
		/// <param name="quotedStrings">Flag indicating whether or not strings in the data are quoted</param>
		/// <param name="delimiter">Column delimiter characters</param>
		/// <param name="hasColumnHeadings">Flag indicating whether or not the first row of data is column headings</param>
		/// <param name="columnTypes">Array of types of the columns of the data</param>
		public CharacterDelimitedDataReader(TextReader reader, bool quotedStrings, string delimiter, bool hasColumnHeadings, params Type[] columnTypes) : this(reader, delimiter, hasColumnHeadings, columnTypes)
		{
			_quotedStrings = quotedStrings;
		}

        /// <summary>
        /// Constructor for CharacterDelimitedDataReader. 
        /// </summary>
        /// <remarks>
        /// This constructor will ignore column headings if the data has 
        /// them and instead just use the field array.  This just 
        /// allows you to skip the first row if you want.
        /// </remarks>
        /// <param name="reader">TextReader for stream of data</param>
        /// <param name="quotedStrings">Flag indicating whether or not strings in the data are quoted</param>
        /// <param name="delimiter">Column delimiter characters</param>
        /// <param name="hasColumnHeadings">Flag indicating whether or not the first row of data is column headings</param>
        /// <param name="fields">Array of fields in the data</param>
        public CharacterDelimitedDataReader(TextReader reader, bool quotedStrings, string delimiter, bool hasColumnHeadings, params Field[] fields)
            : this(reader, quotedStrings, delimiter, fields)
        {
            //throw away the first line...
            if (hasColumnHeadings) ReadHeadings(0);
        }

		#endregion

        /// <summary>
        /// Column delimiter for this reader
        /// </summary>
        public string ColumnDelimiter { get { return _delimiter; } }
	
        private string _line;

        private void LoadFields()
        {
            _line = TextReader.ReadLine();

            if (_line == null)
                return;

            _fields = CalculateFields(_line, false, _quotedStrings, _delimiter);
        }
	
		/// <summary>
		/// Gets next record from stream
		/// </summary>
		/// <returns>IDataRecord for next record or null if none</returns>
		protected override IDataRecord GetNextDataRecord()
		{
            if (RecordsAffected == -1 && _fields == null) LoadFields();

			string record = _line;

            if (record == null)
                record = TextReader.ReadLine();
            else
                _line = null;

            
			if ((_fields.Length > 1 && string.IsNullOrEmpty(record)) || (_fields.Length == 1 && record == null))
				return null;

#if TRACE
            Trace.TraceInformation("Parsing record (quoted strings={1}, delim='{2}', field count={3}):\n\"{0}\"", record, _quotedStrings, _delimiter, _fields.Length);
#endif
			
            string[] values = SplitRecord(record, _quotedStrings, _delimiter, _fields.Length);

			return new ObjectArrayDataRecord(values, _fields);
		}

        /// <remarks />
        protected override Field[] DataFields
        {
            get 
            {
                if (_fields == null)
                    LoadFields();

                return _fields; 
            }
        }

        #region Static constructor helpers

        /// <summary>
        /// Calculate the fields by the first row if none were passed and no headers in file
        /// </summary>
        /// <param name="record">Record of data</param>
        /// <param name="lineIsHeader">Flag indicating the passed line has header names</param>
        /// <param name="quotedStrings">Flag indicating whether or not quoted strings are present</param>
        /// <param name="delimiter">Delimiter character</param>
        /// <returns>Calculated fields</returns>
        internal static Field[] CalculateFields(string record, bool lineIsHeader, bool quotedStrings, string delimiter)
        {
            string[] vals = SplitRecord(record, quotedStrings, delimiter, int.MaxValue);

            Field[] fields = new Field[vals.Length];

            for (int i = 0; i < vals.Length; i++)
            {
                string header = string.Format("Column{0}", i);

                if (lineIsHeader)
                {
                    if (!string.IsNullOrEmpty(header)) header = vals[i];
                }

                fields[i] = new Field(header, typeof(string));
            }

            return fields;
        }

        internal static string[] SplitRecord(string record, bool quotedStrings, string delimiter, int maxValues)
        {
            string[] vals;

            //use regex split or built in string split (quoted strings or not?)
            if (!quotedStrings)
                vals = record.Split(new string[] { delimiter }, maxValues, StringSplitOptions.None);
            else
                vals = StringHelper.QuotedSplit(record, delimiter, maxValues);

            return vals;
        }

        #endregion
    }
}
